use std::fmt::Display;
use std::{i16, i32, i64, i8, isize};
use std::{u16, u32, u64, u8, usize};

macro_rules! impl_signed_as_unsigned(($fn_name:ident, $from:ident, $to:ident) => {
    #[inline]
    fn $fn_name(self) -> Option<$to> {
        if self < 0 {
            return None;
        }
        if $from::MAX as f64 > $to::MAX as f64 {
            if self > $to::MAX as $from {
                return None;
            }
        }
        Some(self as $to)
    }
});

macro_rules! impl_signed_as_signed(($fn_name:ident, $from:ident, $to:ident) => {
    #[inline]
    fn $fn_name(self) -> Option<$to> {

        if $from::MAX as f64 > $to::MAX as f64 {
            if self > $to::MAX as $from {
                return None;
            }
            if self < $to::MIN as $from {
                return None;
            }
        }

        Some(self as $to)
    }
});

macro_rules! impl_unsigned_as_any(($fn_name:ident, $from: ident, $to:ident) => {
    #[inline]
    fn $fn_name(self) -> Option<$to> {
        if $from::MAX as f64 > $to::MAX as f64 {
            if self > $to::MAX as $from {
                return None;
            }
        }
        Some(self as $to)
    }
});

pub trait CheckedIntCast {
  fn as_isize_checked(self) -> Option<isize>;
  fn as_i8_checked(self) -> Option<i8>;
  fn as_i16_checked(self) -> Option<i16>;
  fn as_i32_checked(self) -> Option<i32>;
  fn as_i64_checked(self) -> Option<i64>;
  fn as_usize_checked(self) -> Option<usize>;
  fn as_u8_checked(self) -> Option<u8>;
  fn as_u16_checked(self) -> Option<u16>;
  fn as_u32_checked(self) -> Option<u32>;
  fn as_u64_checked(self) -> Option<u64>;
}

macro_rules! impl_signed_to_all(($from:ident) => {
    impl CheckedIntCast for $from {
        impl_signed_as_signed!(as_isize_checked, $from , isize);
        impl_signed_as_signed!(as_i8_checked, $from , i8);
        impl_signed_as_signed!(as_i16_checked, $from , i16);
        impl_signed_as_signed!(as_i32_checked, $from , i32);
        impl_signed_as_signed!(as_i64_checked, $from , i64);
        impl_signed_as_unsigned!(as_usize_checked, $from , usize);
        impl_signed_as_unsigned!(as_u8_checked, $from , u8);
        impl_signed_as_unsigned!(as_u16_checked, $from , u16);
        impl_signed_as_unsigned!(as_u32_checked, $from , u32);
        impl_signed_as_unsigned!(as_u64_checked, $from , u64);
    }
});

macro_rules! impl_unsigned_to_all(($from:ident) => {
    impl CheckedIntCast for $from {
        impl_unsigned_as_any!(as_isize_checked, $from , isize);
        impl_unsigned_as_any!(as_i8_checked, $from , i8);
        impl_unsigned_as_any!(as_i16_checked, $from , i16);
        impl_unsigned_as_any!(as_i32_checked, $from , i32);
        impl_unsigned_as_any!(as_i64_checked, $from , i64);
        impl_unsigned_as_any!(as_usize_checked, $from , usize);
        impl_unsigned_as_any!(as_u8_checked, $from , u8);
        impl_unsigned_as_any!(as_u16_checked, $from , u16);
        impl_unsigned_as_any!(as_u32_checked, $from , u32);
        impl_unsigned_as_any!(as_u64_checked, $from , u64);
    }
});

impl_signed_to_all!(isize);
impl_signed_to_all!(i8);
impl_signed_to_all!(i16);
impl_signed_to_all!(i32);
impl_signed_to_all!(i64);

impl_unsigned_to_all!(usize);
impl_unsigned_to_all!(u8);
impl_unsigned_to_all!(u16);
impl_unsigned_to_all!(u32);
impl_unsigned_to_all!(u64);

pub trait Truncate {
  fn truncate_as_u8(self) -> u8;
}

impl Truncate for u16 {
  fn truncate_as_u8(self) -> u8 {
    (self & 0xff) as u8
  }
}

pub trait As {
  fn as_u16(self) -> u16;
  fn as_i16(self) -> i16;
  fn as_u32(self) -> u32;
  fn as_usize(self) -> usize;
  fn as_isize(self) -> isize;
}

trait ExpectOrOverflow {
  type Output;
  fn expect_or_overflow<D: Display>(self, value: D, ty: &str) -> Self::Output;
}

impl<T> ExpectOrOverflow for Option<T> {
  type Output = T;
  fn expect_or_overflow<D: Display>(self, value: D, ty: &str) -> Self::Output {
    match self {
      Some(v) => v,
      None => panic!("{} overflows {}", value, ty),
    }
  }
}

macro_rules! impl_as {
  ($ty:ty) => {
    impl As for $ty {
      fn as_u16(self) -> u16 {
        self.as_u16_checked().expect_or_overflow(self, "u16")
      }

      fn as_i16(self) -> i16 {
        self.as_i16_checked().expect_or_overflow(self, "i16")
      }

      fn as_u32(self) -> u32 {
        self.as_u32_checked().expect_or_overflow(self, "u32")
      }

      fn as_usize(self) -> usize {
        self.as_usize_checked().expect_or_overflow(self, "usize")
      }

      fn as_isize(self) -> isize {
        self.as_isize_checked().expect_or_overflow(self, "isize")
      }
    }
  };
}

impl_as!(i16);
impl_as!(u32);
impl_as!(usize);
impl_as!(isize);
